# SimpleBeanCache.java

    package org.codefilarete.stalactite.sql.result;

    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Function;

    /**
     * Simple class to ease access or creation to entity from the cache.
     * A cache organized per bean class, then per bean identifier.
     *
     * @see #computeIfAbsent(Class, Object, Function)
     */
    public final class SimpleBeanCache {

    	/** Delegate bean cache : per type, then per identifier */
    	private final Map<Class, Map<BeanKey /* bean key */, Object>> entityCache;

    	public SimpleBeanCache() {
    		this(new HashMap<>());
    	}

    	public SimpleBeanCache(Map<Class, Map<BeanKey, Object>> entityCache) {
    		this.entityCache = entityCache;
    	}

    	/**
    	 * Clears the cache
    	 */
    	public void clear() {
    		this.entityCache.clear();
    	}

    	/**
    	 * Main method that tries to retrieve an entity by its class and identifier or instantiates it and put it into the cache
    	 *
    	 * @param clazz the type of the entity
    	 * @param identifier the identifier of the entity (Long, String, ...),
    	 * 				not null (because null has no purpose here), can be an array (for composed key case)
    	 * @param factory the "method" that will be called to create the entity when the entity is not in the cache
    	 * @return the existing instance in the cache or a new object
    	 */
    	public <C, I> C computeIfAbsent(Class<C> clazz, I identifier, Function<I, C> factory) {
    		BeanKey key;
    		if (identifier.getClass().isArray()) {
    			// NB: we must cast into Object[] to avoid the JVM to wrap the identifier into an array of Object due to varargs constructor
    			key = new BeanKey((Object[]) identifier);
    		} else {
    			key = new BeanKey(identifier);
    		}
    		C rowInstance = (C) entityCache.computeIfAbsent(clazz, k -> new HashMap<>()).get(key);
    		if (rowInstance == null) {
    			rowInstance = factory.apply(identifier);
    			entityCache.computeIfAbsent(clazz, k -> new HashMap<>()).put(key, rowInstance);
    		}
    		return rowInstance;
    	}

    	/**
    	 * Wrapper for {@link Map} key to take arrays into account : array's hashcode are not computed on their content (as the opposite of List)
    	 * hence two arrays having same content are not under the same hashcode. This class computes hashcode arrays on their content, thus, two
    	 * beans with same composed keys hit the cache.
    	 */
    	private static class BeanKey {

    		private final Object[] keys;
    		/** array hashCode, introduced for optimization: prevent from computing it at each hashCode() method call */
    		private final int hashCode;

    		private BeanKey(Object ... keys) {
    			this.keys = keys;
    			this.hashCode = Arrays.hashCode(keys);;
    		}

    		@Override
    		public boolean equals(Object o) {
    			if (this == o) return true;
    			if (!(o instanceof BeanKey)) return false;
    			BeanKey beanKey = (BeanKey) o;
    			return Arrays.equals(keys, beanKey.keys);
    		}

    		@Override
    		public int hashCode() {
    			return hashCode;
    		}
    	}
    }